Skip to content

04 JS 深拷贝

  • 深拷贝(deep copy)是指完全复制一个对象及其所有子对象,使得新对象与原对象互相独立,修改新对象不会影响原对象 (新对象与原对象不共享任何内存空间。)。
  • 相对于浅拷贝(shallow copy),深拷贝不仅复制对象本身,还递归地复制所有嵌套的子对象。

JSON 方法

最简单且常用的方法是利用 JSON.stringifyJSON.parse。这是最简单的一种实现深拷贝的方法,但它有一些限制:

  • 不能拷贝函数、undefinedSymbol
  • 不能正确处理 Date 对象(会被转换成字符串)。
  • 不能处理正则表达式对象。
  • 不能处理循环引用。
js
function deepClone(obj) {
  return JSON.parse(JSON.stringify(obj));
}

const obj1 = { a: 1, b: { c: 2 } };
const obj2 = deepClone(obj1);

console.log(obj2 !== obj1); // true
console.log(obj2.b !== obj1.b); // true

obj2.b.c = 3;
console.log(obj1.b.c); // 输出:2

递归手动拷贝

递归函数可以处理大多数情况下的深拷贝,包括数组和对象。

WARNING

在深拷贝时,如果不处理循环引用,递归调用会进入无限循环,最终导致栈溢出错误。

js
function deepClone(obj, hash = new WeakMap()) {
  // 基本类型(如字符串、数字)或 null 直接返回
  if (obj === null) return null;
  if (typeof obj !== 'object') return obj;

  // 处理日期对象,创建新的实例返回
  if (obj instanceof Date) return new Date(obj);

  // 处理正则表达式对象,创建新的实例返回
  if (obj instanceof RegExp) return new RegExp(obj);

  // 处理循环引用,若已存在于 hash 中则返回记录的副本
  // 使用 WeakMap 跟踪已经拷贝的对象,避免循环引用导致的无限递归
  if (hash.has(obj)) return hash.get(obj);

  // 创建新的对象或数组
  // 使用 obj.constructor 创建与原对象相同类型的新实例,并将其存入 WeakMap
  let cloneObj = new obj.constructor();
  hash.set(obj, cloneObj);

  // 遍历对象的所有属性
  // 遍历对象的所有自身属性(不包括原型链上的属性),递归调用 deepClone 进行拷贝
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 递归拷贝属性
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }

  return cloneObj;
}

// 测试代码
const obj1 = {
  a: 1,
  b: { c: 2 },
  d: new Date(),
  e: /test/i,
  f: function() { return 'hello'; },
  g: [1, 2, 3]
};
obj1.self = obj1; // 创建循环引用,用于测试

const obj2 = deepClone(obj1);
console.log(obj2);

console.log(obj2 !== obj1); // true
console.log(obj2.b !== obj1.b); // true

obj2.b.c = 3;
console.log(obj1.b.c); // 输出:2

使用第三方库

使用第三方库,如 Lodash 提供的 _.cloneDeep 方法,它是一个广泛使用的工具函数,可以处理大多数深拷贝的场景。

js
const _ = require('lodash');

const obj1 = { a: 1, b: { c: 2 } };
const obj2 = _.cloneDeep(obj1);

console.log(obj2 !== obj1); // true
console.log(obj2.b !== obj1.b); // true

obj2.b.c = 3;
console.log(obj1.b.c); // 输出:2

现代浏览器的结构化克隆算法(structuredClone)

structuredClone 是一种内置的方法,专门用于深拷贝。

JSON.stringify()JSON.parse() 方法相比,structuredClone 有以下优势:

  • 它能够处理循环引用。
  • 它能够正确地复制 Date 对象、RegExp 对象、MapSetBlobFileListImageBitmapImageData 等特殊对象。
  • 它保留了对象的类型信息,不需要像 JSON 方法那样转换对象类型。
  • 它的性能通常比 JSON 方法要好,因为它不需要将对象序列化和反序列化。

基本用法

structuredClone 的一些特性:

  • 不可转移的值:如果对象中包含不可转移的值(如函数、Symbol、Promise、WeakMap、WeakSet 等),则 structuredClone 会抛出异常。
  • 循环引用structuredClone 能够正确处理循环引用的情况。
  • 传输数据structuredClone 可以用于在不同的执行上下文(例如,不同的 Web Workers)之间传输数据。
js
const original = {
  num: 0,
  str: 'string',
  bool: true,
  nul: null,
  und: undefined,
  obj: { id: 'object' },
  arr: [0, 1, 2],
  date: new Date(),
  reg: new RegExp('/reg/'),
  map: new Map([['key', 'value']]),
  set: new Set([1, 2, 3]),
};

const clone = structuredClone(original);

console.log(clone); // { num: 0, str: 'string', bool: true, nul: null, und: undefined, obj: { id: 'object' }, arr: [0, 1, 2], date: 2023-10-10T16:00:00.000Z, reg: /\/reg\//, map: Map(1) { 'key' => 'value' }, set: Set(3) { 1, 2, 3 } }
console.log(clone !== original); // true
console.log(clone.b !== original.b); // true
console.log(clone.d !== original.d); // true

处理循环引用

js
const obj = {};
obj.self = obj; // 循环引用

const clone = structuredClone(obj);
console.log(clone.self === clone); // true,说明循环引用被正确处理